//! Compatibility with the current stable Nickel version.
//!
//! This module defines a trait for converting to and from the representation used in stable Nickel
//! to the new AST representation of the bytecode compiler, and implements it for the types defined
//! in [nickel_lang_parser::ast].

use super::{primop::PrimOp, *};
use crate::{
    combine::Combine,
    eval::value::{
        ArrayData, Container, EnumVariantData, NickelValue, TypeData, ValueContentRef,
        ValueContentRefMut,
    },
    label,
    position::{PosTable, RawSpan},
    term::{self, pattern::compile::Compile as _},
    typ as mline_type,
};

use nickel_lang_parser::{
    ast::{
        self,
        pattern::{Pattern, PatternData},
        record::Record,
        typ::{
            EnumRow, EnumRows, EnumRowsUnr, RecordRow, RecordRows, RecordRowsUnr, Type, TypeUnr,
        },
    },
    error::ParseError,
    identifier::LocIdent,
    typ::{EnumRowF, RecordRowF},
};

use indexmap::IndexMap;
use smallvec::SmallVec;
use std::rc::Rc;

/// Convert from the mainline Nickel representation to the new AST representation. This trait is
/// mostly `From` with an additional argument for the allocator.
///
/// # Parameters
///
/// - `'ast`: the lifetime of the AST nodes, tied to the allocator
/// - `'a`: the lifetime of the reference to the mainline Nickel object, which doesn't need to be
///   related to `'ast` (we will copy any required data into the allocator)
/// - `T`: the type of the mainline Nickel object ([term::Term], etc.)
pub trait FromMainline<'ast, T> {
    fn from_mainline(alloc: &'ast AstAlloc, pos_table: &PosTable, mainline: &T) -> Self;
}

impl<'ast> FromMainline<'ast, term::TypeAnnotation> for Annotation<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        annot: &term::TypeAnnotation,
    ) -> Self {
        let typ = annot
            .typ
            .as_ref()
            .map(|typ| typ.typ.to_ast(alloc, pos_table));

        let contracts = alloc.alloc_many(
            annot
                .contracts
                .iter()
                .map(|contract| contract.typ.to_ast(alloc, pos_table)),
        );

        Annotation { typ, contracts }
    }
}

impl<'ast> FromMainline<'ast, (LocIdent, term::record::Field)> for record::FieldDef<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        (id, content): &(LocIdent, term::record::Field),
    ) -> Self {
        let pos = content
            .value
            .as_ref()
            .map(|value| id.pos.fuse(pos_table.get(value.pos_idx())))
            .unwrap_or(id.pos);

        record::FieldDef {
            path: alloc.alloc_singleton(record::FieldPathElem::Ident(*id)),
            value: content
                .value
                .as_ref()
                .map(|term| term.to_ast(alloc, pos_table)),
            metadata: content.metadata.to_ast(alloc, pos_table),
            pos,
        }
    }
}

impl<'ast> FromMainline<'ast, term::record::FieldMetadata> for record::FieldMetadata<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        metadata: &term::record::FieldMetadata,
    ) -> Self {
        let doc = metadata.doc.as_ref().map(|doc| alloc.alloc_str(doc));

        record::FieldMetadata {
            doc,
            annotation: metadata.annotation.to_ast(alloc, pos_table),
            opt: metadata.opt,
            not_exported: metadata.not_exported,
            priority: metadata.priority.clone(),
        }
    }
}

impl<'ast> FromMainline<'ast, term::record::SharedMetadata> for record::FieldMetadata<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        metadata: &term::record::SharedMetadata,
    ) -> Self {
        metadata
            .0
            .as_ref()
            .map(|metadata| (**metadata).to_ast(alloc, pos_table))
            .unwrap_or_default()
    }
}

impl<'ast> FromMainline<'ast, term::record::Include> for record::Include<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        mainline: &term::record::Include,
    ) -> Self {
        record::Include {
            ident: mainline.ident,
            metadata: mainline.metadata.to_ast(alloc, pos_table),
        }
    }
}

impl<'ast> FromMainline<'ast, mline_type::Type> for Type<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        mainline: &mline_type::Type,
    ) -> Self {
        Type {
            typ: mainline.typ.to_ast(alloc, pos_table),
            pos: mainline.pos,
        }
    }
}

type MainlineTypeUnr = mline_type::TypeF<
    Box<mline_type::Type>,
    mline_type::RecordRows,
    mline_type::EnumRows,
    NickelValue,
>;

impl<'ast> FromMainline<'ast, MainlineTypeUnr> for TypeUnr<'ast> {
    fn from_mainline(alloc: &'ast AstAlloc, pos_table: &PosTable, typ: &MainlineTypeUnr) -> Self {
        typ.clone().map(
            |typ| alloc.alloc((*typ).to_ast(alloc, pos_table)),
            |rrows| rrows.to_ast(alloc, pos_table),
            |erows| erows.to_ast(alloc, pos_table),
            |ctr| ctr.to_ast(alloc, pos_table),
        )
    }
}

impl<'ast> FromMainline<'ast, mline_type::RecordRows> for RecordRows<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        rrows: &mline_type::RecordRows,
    ) -> Self {
        RecordRows(rrows.0.to_ast(alloc, pos_table))
    }
}

impl<'ast> FromMainline<'ast, mline_type::EnumRows> for EnumRows<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        erows: &mline_type::EnumRows,
    ) -> Self {
        EnumRows(erows.0.to_ast(alloc, pos_table))
    }
}

type MainlineEnumRowsUnr = mline_type::EnumRowsF<Box<mline_type::Type>, Box<mline_type::EnumRows>>;

impl<'ast> FromMainline<'ast, MainlineEnumRowsUnr> for EnumRowsUnr<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        erows: &MainlineEnumRowsUnr,
    ) -> Self {
        erows.clone().map(
            |typ| alloc.alloc((*typ).to_ast(alloc, pos_table)),
            |erows| alloc.alloc((*erows).to_ast(alloc, pos_table)),
        )
    }
}

type MainlineRecordRowsUnr =
    mline_type::RecordRowsF<Box<mline_type::Type>, Box<mline_type::RecordRows>>;

impl<'ast> FromMainline<'ast, MainlineRecordRowsUnr> for RecordRowsUnr<'ast> {
    fn from_mainline(
        alloc: &'ast AstAlloc,
        pos_table: &PosTable,
        rrows: &MainlineRecordRowsUnr,
    ) -> Self {
        rrows.clone().map(
            |typ| alloc.alloc((*typ).to_ast(alloc, pos_table)),
            |rrows| alloc.alloc((*rrows).to_ast(alloc, pos_table)),
        )
    }
}

impl<'ast> FromMainline<'ast, term::Term> for Node<'ast> {
    fn from_mainline(alloc: &'ast AstAlloc, pos_table: &PosTable, term: &term::Term) -> Self {
        use term::Term;

        match term {
            Term::StrChunks(chunks) => {
                alloc.string_chunks(chunks.iter().rev().map(|chunk| match chunk {
                    term::StrChunk::Literal(s) => StringChunk::Literal(s.clone()),
                    term::StrChunk::Expr(expr, indent) => {
                        StringChunk::Expr(expr.to_ast(alloc, pos_table), *indent)
                    }
                }))
            }
            Term::Fun(data) => {
                let fst_arg = Pattern::any(data.arg);
                let body = &data.body;

                let mut args = vec![fst_arg];
                let mut maybe_next_fun = body;

                let final_body = loop {
                    match maybe_next_fun.as_term() {
                        Some(Term::Fun(data)) => {
                            args.push(Pattern::any(data.arg));
                            maybe_next_fun = &data.body;
                        }
                        _ => break maybe_next_fun,
                    }
                };

                alloc.fun(args, final_body.to_ast(alloc, pos_table))
            }
            Term::Let(data) => alloc.let_block(
                data.bindings.iter().map(|(id, value)| LetBinding {
                    pattern: Pattern::any(*id),
                    value: value.to_ast(alloc, pos_table),
                    metadata: Default::default(),
                }),
                data.body.to_ast(alloc, pos_table),
                data.attrs.rec,
            ),
            Term::App(data) => {
                // We have to special-case if-then-else, which is encoded as a primop application
                // of the unary operator if-then-else to the condition, followed by two normal
                // applications for the if and else branches, which is a bit painful to match.
                if let Some(Term::App(data_inner)) = data.head.as_term()
                    && let Some(Term::Op1(data)) = data_inner.head.as_term()
                    && data.op == term::UnaryOp::IfThenElse
                {
                    return alloc.if_then_else(
                        data.arg.to_ast(alloc, pos_table),
                        data_inner.arg.to_ast(alloc, pos_table),
                        data.arg.to_ast(alloc, pos_table),
                    );
                }

                let mut args = vec![data.arg.to_ast(alloc, pos_table)];
                let mut maybe_next_app = data.head.as_term();

                while let Some(Term::App(data_next)) = maybe_next_app {
                    args.push(data_next.arg.to_ast(alloc, pos_table));
                    maybe_next_app = data_next.head.as_term();
                }

                // Application is left-associative: `f x y` is parsed as `(f x) y`. So we see the
                // outer argument `y` first, and `args` will be `[y, x]`. We need to reverse it to
                // match the expected order of a flat application node.
                args.reverse();
                alloc.app(data.head.to_ast(alloc, pos_table), args)
            }
            Term::Var(id) => Node::Var(*id),
            Term::RecRecord(data) => {
                let mut field_defs = Vec::new();

                field_defs.extend(data.record.fields.iter().map(|(id, field)| {
                    let pos = field
                        .value
                        .as_ref()
                        .map(|value| id.pos.fuse(value.pos(pos_table)))
                        .unwrap_or(id.pos);

                    record::FieldDef {
                        path: record::FieldPathElem::single_ident_path(alloc, *id),
                        value: field
                            .value
                            .as_ref()
                            .map(|term| term.to_ast(alloc, pos_table)),
                        metadata: field.metadata.to_ast(alloc, pos_table),
                        pos,
                    }
                }));

                field_defs.extend(data.dyn_fields.iter().map(|(expr, field)| {
                    let pos_field_name = expr.pos(pos_table);
                    let pos = field
                        .value
                        .as_ref()
                        .map(|v| pos_field_name.fuse(v.pos(pos_table)))
                        .unwrap_or(pos_field_name);

                    record::FieldDef {
                        path: record::FieldPathElem::single_expr_path(
                            alloc,
                            expr.to_ast(alloc, pos_table),
                        ),
                        metadata: field.metadata.to_ast(alloc, pos_table),
                        value: field
                            .value
                            .as_ref()
                            .map(|term| term.to_ast(alloc, pos_table)),
                        pos,
                    }
                }));

                alloc.record(Record {
                    field_defs: alloc.alloc_many(field_defs),
                    includes: alloc.alloc_many(
                        data.includes
                            .iter()
                            .map(|incl| incl.to_ast(alloc, pos_table)),
                    ),
                    open: data.record.attrs.open,
                })
            }
            Term::Op1(data) => alloc.prim_op(
                PrimOp::from(&data.op),
                std::iter::once(data.arg.to_ast(alloc, pos_table)),
            ),
            Term::Op2(data) => {
                // [^primop-argument-order]: Some primops have had exotic arguments order for
                // historical reasons. The new AST tries to follow the stdlib argument order
                // whenever possible, which means we have to swap the arguments for a few primops.

                let op = PrimOp::from(&data.op);
                let mut args = [
                    data.arg1.to_ast(alloc, pos_table),
                    data.arg2.to_ast(alloc, pos_table),
                ];

                if matches!(op, PrimOp::ArrayAt | PrimOp::StringContains) {
                    args.swap(0, 1);
                }

                alloc.prim_op(op, args)
            }
            Term::OpN(data) => {
                // See [^primop-argument-order].
                let op = PrimOp::from(&data.op);
                let mut args: Vec<_> = data
                    .args
                    .iter()
                    .map(|arg| arg.to_ast(alloc, pos_table))
                    .collect();

                if let PrimOp::StringSubstr = op {
                    debug_assert_eq!(args.len(), 3);
                    // The original order is: the string, then start and end.
                    // The target order is: start, end and then the string.
                    args.swap(0, 1);
                    args.swap(1, 2);
                }

                alloc.prim_op(op, args)
            }
            Term::Sealed(..) => panic!("didn't expect a sealed term at the first stage"),
            Term::Annotated(data) => alloc.annotated(
                (*data.annot).to_ast(alloc, pos_table),
                data.inner.to_ast(alloc, pos_table),
            ),
            Term::Import(term::Import::Path { path, format }) => {
                alloc.import_path(path.clone(), *format)
            }
            Term::Import(term::Import::Package { id }) => alloc.import_package(*id),
            Term::ResolvedImport(_) => panic!("didn't expect a resolved import at parsing stage"),
            Term::ParseError(error) => alloc.parse_error((**error).clone()),
            Term::RuntimeError(_) => panic!("didn't expect a runtime error at parsing stage"),
            Term::Closurize(value) => Ast::from_mainline(alloc, pos_table, value).node,
        }
    }
}

impl<'ast> FromMainline<'ast, NickelValue> for Ast<'ast> {
    fn from_mainline(alloc: &'ast AstAlloc, pos_table: &PosTable, rterm: &NickelValue) -> Self {
        let node = match rterm.content_ref() {
            ValueContentRef::Null => Node::Null,
            ValueContentRef::Bool(b) => Node::Bool(b),
            ValueContentRef::Number(n) => alloc.number(n.clone()),
            ValueContentRef::Array(Container::Empty) => Node::Array(&[]),
            // We ignore pending contracts here. Conversion between ASTs should happen before
            // runtime, when `pending_contracts` is empty.
            ValueContentRef::Array(Container::Alloc(ArrayData {
                array,
                pending_contracts: _,
            })) => {
                // We should probably make array's iterator an ExactSizeIterator. But for now, we
                // don't care about the translation's performance so it's simpler to just collect
                // them in a vec locally.
                let elts = array
                    .iter()
                    .map(|elt| elt.to_ast(alloc, pos_table))
                    .collect::<Vec<_>>();
                alloc.array(elts)
            }
            ValueContentRef::Record(Container::Empty) => alloc.record(Record::empty()),
            ValueContentRef::Record(Container::Alloc(data)) => {
                let field_defs = alloc.alloc_many(data.fields.iter().map(|(id, field)| {
                    let pos = field
                        .value
                        .as_ref()
                        .map(|value| id.pos.fuse(value.pos(pos_table)))
                        .unwrap_or(id.pos);

                    record::FieldDef {
                        path: record::FieldPathElem::single_ident_path(alloc, *id),
                        value: field
                            .value
                            .as_ref()
                            .map(|term| term.to_ast(alloc, pos_table)),
                        metadata: field.metadata.to_ast(alloc, pos_table),
                        pos,
                    }
                }));

                alloc.record(Record {
                    field_defs,
                    includes: &[],
                    open: data.attrs.open,
                })
            }
            ValueContentRef::String(s) => alloc.string(s),
            ValueContentRef::Term(term) => term.to_ast(alloc, pos_table),
            ValueContentRef::EnumVariant(EnumVariantData { tag, arg }) => {
                alloc.enum_variant(*tag, arg.as_ref().map(|arg| arg.to_ast(alloc, pos_table)))
            }
            // We ignore the cached contract at this stage (it shouldn't be set)
            ValueContentRef::Type(TypeData { typ, contract: _ }) => {
                alloc.typ(typ.to_ast(alloc, pos_table))
            }
            // Those aren't representable in the new AST. We should have a better error than
            // "unimplemented", but I'm not sure we should bother make the whole process fallible
            // because of that.
            ValueContentRef::Label(_)
            | ValueContentRef::CustomContract(_)
            | ValueContentRef::ForeignId(_)
            | ValueContentRef::SealingKey(_)
            | ValueContentRef::Thunk(_) => unimplemented!(),
        };

        Ast {
            node,
            pos: rterm.pos(pos_table),
        }
    }
}

impl<'ast> FromMainline<'ast, NickelValue> for &'ast Ast<'ast> {
    fn from_mainline(alloc: &'ast AstAlloc, pos_table: &PosTable, rterm: &NickelValue) -> Self {
        alloc.alloc(rterm.to_ast(alloc, pos_table))
    }
}

/// Symmetric to `FromMainline`, as `Into` is to `From`.
pub trait ToAst<'ast, T> {
    fn to_ast(&self, alloc: &'ast AstAlloc, pos_table: &PosTable) -> T;
}

impl<'ast, S, T> ToAst<'ast, T> for S
where
    T: FromMainline<'ast, S>,
{
    fn to_ast(&self, alloc: &'ast AstAlloc, pos_table: &PosTable) -> T {
        T::from_mainline(alloc, pos_table, self)
    }
}

// Primops don't need any heap allocation, so we can implement `From` directly.
impl From<&term::UnaryOp> for PrimOp {
    fn from(op: &term::UnaryOp) -> Self {
        match op {
            term::UnaryOp::IfThenElse => {
                panic!("if-then-else should have been handed separately by special casing")
            }
            term::UnaryOp::Typeof => PrimOp::Typeof,
            term::UnaryOp::Cast => PrimOp::Cast,
            term::UnaryOp::BoolAnd => PrimOp::BoolAnd,
            term::UnaryOp::BoolOr => PrimOp::BoolOr,
            term::UnaryOp::BoolNot => PrimOp::BoolNot,
            term::UnaryOp::Blame => PrimOp::Blame,
            term::UnaryOp::EnumEmbed(loc_ident) => PrimOp::EnumEmbed(*loc_ident),
            term::UnaryOp::RecordAccess(loc_ident) => PrimOp::RecordStatAccess(*loc_ident),
            term::UnaryOp::ArrayMap => PrimOp::ArrayMap,
            term::UnaryOp::RecordMap => PrimOp::RecordMap,
            term::UnaryOp::LabelFlipPol => PrimOp::LabelFlipPol,
            term::UnaryOp::LabelPol => PrimOp::LabelPol,
            term::UnaryOp::LabelGoDom => PrimOp::LabelGoDom,
            term::UnaryOp::LabelGoCodom => PrimOp::LabelGoCodom,
            term::UnaryOp::LabelGoArray => PrimOp::LabelGoArray,
            term::UnaryOp::LabelGoDict => PrimOp::LabelGoDict,
            term::UnaryOp::Seq => PrimOp::Seq,
            term::UnaryOp::DeepSeq => PrimOp::DeepSeq,
            term::UnaryOp::ArrayLength => PrimOp::ArrayLength,
            term::UnaryOp::ArrayGen => PrimOp::ArrayGen,
            term::UnaryOp::RecordFields(record_op_kind) => PrimOp::RecordFields(*record_op_kind),
            term::UnaryOp::RecordValues => PrimOp::RecordValues,
            term::UnaryOp::StringTrim => PrimOp::StringTrim,
            term::UnaryOp::StringChars => PrimOp::StringChars,
            term::UnaryOp::StringUppercase => PrimOp::StringUppercase,
            term::UnaryOp::StringLowercase => PrimOp::StringLowercase,
            term::UnaryOp::StringLength => PrimOp::StringLength,
            term::UnaryOp::ToString => PrimOp::ToString,
            term::UnaryOp::NumberFromString => PrimOp::NumberFromString,
            term::UnaryOp::EnumFromString => PrimOp::EnumFromString,
            term::UnaryOp::StringIsMatch => PrimOp::StringIsMatch,
            term::UnaryOp::StringFind => PrimOp::StringFind,
            term::UnaryOp::StringFindAll => PrimOp::StringFindAll,
            term::UnaryOp::Force {
                ignore_not_exported,
            } => PrimOp::Force {
                ignore_not_exported: *ignore_not_exported,
            },
            term::UnaryOp::RecordEmptyWithTail => PrimOp::RecordEmptyWithTail,
            term::UnaryOp::RecordFreeze => PrimOp::RecordFreeze,
            term::UnaryOp::Trace => PrimOp::Trace,
            term::UnaryOp::LabelPushDiag => PrimOp::LabelPushDiag,
            #[cfg(feature = "nix-experimental")]
            term::UnaryOp::EvalNix => PrimOp::EvalNix,
            term::UnaryOp::EnumGetArg => PrimOp::EnumGetArg,
            term::UnaryOp::EnumMakeVariant => PrimOp::EnumMakeVariant,
            term::UnaryOp::EnumIsVariant => PrimOp::EnumIsVariant,
            term::UnaryOp::EnumGetTag => PrimOp::EnumGetTag,
            term::UnaryOp::ContractCustom => PrimOp::ContractCustom,
            term::UnaryOp::NumberArcCos => PrimOp::NumberArcCos,
            term::UnaryOp::NumberArcSin => PrimOp::NumberArcSin,
            term::UnaryOp::NumberArcTan => PrimOp::NumberArcTan,
            term::UnaryOp::NumberCos => PrimOp::NumberCos,
            term::UnaryOp::NumberSin => PrimOp::NumberSin,
            term::UnaryOp::NumberTan => PrimOp::NumberTan,

            op @ (term::UnaryOp::TagsOnlyMatch { .. }
            | term::UnaryOp::ChunksConcat
            | term::UnaryOp::StringIsMatchCompiled(_)
            | term::UnaryOp::StringFindCompiled(_)
            | term::UnaryOp::StringFindAllCompiled(_)
            | term::UnaryOp::RecDefault
            | term::UnaryOp::RecForce
            | term::UnaryOp::ContractPostprocessResult
            | term::UnaryOp::ContractAttachDefaultLabel) => {
                panic!("didn't expect {op} at the parsing stage")
            }
        }
    }
}

impl From<&term::BinaryOp> for PrimOp {
    fn from(op: &term::BinaryOp) -> Self {
        match op {
            term::BinaryOp::Plus => PrimOp::Plus,
            term::BinaryOp::Sub => PrimOp::Sub,
            term::BinaryOp::Mult => PrimOp::Mult,
            term::BinaryOp::Div => PrimOp::Div,
            term::BinaryOp::Modulo => PrimOp::Modulo,
            term::BinaryOp::NumberArcTan2 => PrimOp::NumberArcTan2,
            term::BinaryOp::NumberLog => PrimOp::NumberLog,
            term::BinaryOp::Pow => PrimOp::Pow,
            term::BinaryOp::StringConcat => PrimOp::StringConcat,
            term::BinaryOp::StringBase64Encode => PrimOp::StringBase64Encode,
            term::BinaryOp::StringBase64Decode => PrimOp::StringBase64Decode,
            term::BinaryOp::Eq => PrimOp::Eq,
            term::BinaryOp::LessThan => PrimOp::LessThan,
            term::BinaryOp::LessOrEq => PrimOp::LessOrEq,
            term::BinaryOp::GreaterThan => PrimOp::GreaterThan,
            term::BinaryOp::GreaterOrEq => PrimOp::GreaterOrEq,
            term::BinaryOp::ContractApply => PrimOp::ContractApply,
            term::BinaryOp::ContractCheck => PrimOp::ContractCheck,
            term::BinaryOp::LabelWithErrorData => PrimOp::LabelWithErrorData,
            term::BinaryOp::LabelGoField => PrimOp::LabelGoField,
            // This corresponds to a call to `%record/insert%` from the source language. Other
            // forms are introduced by the evaluator, e.g. when evaluating a recursive record to a
            // record.
            term::BinaryOp::RecordInsert {
                metadata,
                pending_contracts,
                ext_kind,
                op_kind,
            } if metadata.is_empty()
                && pending_contracts.is_empty()
                && *ext_kind == term::RecordExtKind::WithValue =>
            {
                PrimOp::RecordInsert(*op_kind)
            }
            term::BinaryOp::RecordRemove(record_op_kind) => PrimOp::RecordRemove(*record_op_kind),
            term::BinaryOp::RecordGet => PrimOp::RecordGet,
            term::BinaryOp::RecordHasField(record_op_kind) => {
                PrimOp::RecordHasField(*record_op_kind)
            }
            term::BinaryOp::RecordFieldIsDefined(record_op_kind) => {
                PrimOp::RecordFieldIsDefined(*record_op_kind)
            }
            term::BinaryOp::RecordSplitPair => PrimOp::RecordSplitPair,
            term::BinaryOp::RecordDisjointMerge => PrimOp::RecordDisjointMerge,
            term::BinaryOp::ArrayConcat => PrimOp::ArrayConcat,
            term::BinaryOp::ArrayAt => PrimOp::ArrayAt,
            term::BinaryOp::Merge(merge_label) => PrimOp::Merge(merge_label.kind),
            term::BinaryOp::Hash => PrimOp::Hash,
            term::BinaryOp::Serialize => PrimOp::Serialize,
            term::BinaryOp::Deserialize => PrimOp::Deserialize,
            term::BinaryOp::StringSplit => PrimOp::StringSplit,
            term::BinaryOp::StringContains => PrimOp::StringContains,
            term::BinaryOp::StringCompare => PrimOp::StringCompare,
            term::BinaryOp::ContractArrayLazyApp => PrimOp::ContractArrayLazyApp,
            term::BinaryOp::ContractRecordLazyApp => PrimOp::ContractRecordLazyApp,
            term::BinaryOp::LabelWithMessage => PrimOp::LabelWithMessage,
            term::BinaryOp::LabelWithNotes => PrimOp::LabelWithNotes,
            term::BinaryOp::LabelAppendNote => PrimOp::LabelAppendNote,
            term::BinaryOp::LabelLookupTypeVar => PrimOp::LabelLookupTypeVar,

            term::BinaryOp::Seal => PrimOp::Seal,
            term::BinaryOp::Unseal => PrimOp::Unseal,

            term::BinaryOp::RecordInsert { .. } => {
                panic!("didn't expect %record/insert% at the parsing stage")
            }
        }
    }
}

impl From<&term::NAryOp> for PrimOp {
    fn from(op: &term::NAryOp) -> Self {
        match op {
            term::NAryOp::StringReplace => PrimOp::StringReplace,
            term::NAryOp::StringReplaceRegex => PrimOp::StringReplaceRegex,
            term::NAryOp::StringSubstr => PrimOp::StringSubstr,
            term::NAryOp::MergeContract => PrimOp::MergeContract,
            term::NAryOp::RecordSealTail => PrimOp::RecordSealTail,
            term::NAryOp::RecordUnsealTail => PrimOp::RecordUnsealTail,
            term::NAryOp::LabelInsertTypeVar => PrimOp::LabelInsertTypeVar,
            term::NAryOp::ArraySlice => PrimOp::ArraySlice,
        }
    }
}

/// Trait from converting from the new AST representation to the mainline Nickel representation.
///
/// Note that in that direction, we don't need the allocator: those traits are thus isomorphic to
/// to `From<_>` and `Into<_>` respectively. However, we convert from a reference to an owned
/// value. We initially used `From` directly, but this causes annoying inference issue around auto
/// deref and blanket implementations of `From`/`Into`. It's just simpler and more explicit to have
/// a separate trait for this conversion as well.
pub trait FromAst<T> {
    fn from_ast(ast: &T, pos_table: &mut PosTable) -> Self;
}

pub trait ToMainline<T> {
    fn to_mainline(&self, pos_table: &mut PosTable) -> T;
}

impl<S, T> ToMainline<T> for S
where
    T: FromAst<S>,
{
    fn to_mainline(&self, pos_table: &mut PosTable) -> T {
        T::from_ast(self, pos_table)
    }
}

impl<'ast> FromAst<Annotation<'ast>> for term::TypeAnnotation {
    fn from_ast(annot: &Annotation<'ast>, pos_table: &mut PosTable) -> Self {
        let typ = annot.typ.as_ref().map(|typ| typ.to_mainline(pos_table));

        let contracts = annot
            .contracts
            .iter()
            .map(|ctr| ctr.to_mainline(pos_table))
            .collect();

        term::TypeAnnotation { typ, contracts }
    }
}

impl<'ast> FromAst<StringChunk<Ast<'ast>>> for term::StrChunk<NickelValue> {
    fn from_ast(chunk: &StringChunk<Ast<'ast>>, pos_table: &mut PosTable) -> Self {
        match chunk {
            StringChunk::Literal(s) => term::StrChunk::Literal(s.clone()),
            StringChunk::Expr(expr, indent) => {
                term::StrChunk::Expr(expr.to_mainline(pos_table), *indent)
            }
        }
    }
}

/// Similar to [record::FieldPathElem], but for the mainline representation: either an identifier
/// or a quoted identifier.
pub enum FieldName {
    Ident(LocIdent),
    Expr(NickelValue),
}

impl FromAst<record::FieldPathElem<'_>> for FieldName {
    fn from_ast(elem: &record::FieldPathElem<'_>, pos_table: &mut PosTable) -> Self {
        match elem {
            record::FieldPathElem::Ident(id) => FieldName::Ident(*id),
            record::FieldPathElem::Expr(node) => FieldName::Expr(node.to_mainline(pos_table)),
        }
    }
}

impl<'ast> FromAst<record::FieldDef<'ast>> for (FieldName, term::record::Field) {
    fn from_ast(field: &record::FieldDef<'ast>, pos_table: &mut PosTable) -> Self {
        /// Elaborate a record field definition specified as a path, like `a.b.c = foo`, into a regular
        /// flat definition `a = {b = {c = foo}}`.
        ///
        /// # Preconditions
        /// - /!\ path must be **non-empty**, otherwise this function panics
        use super::record::FieldPathElem;

        // unwrap(): field paths must be non-empty
        let name_innermost = field.path.last().unwrap().try_as_ident();

        let initial = term::record::Field {
            value: field.value.as_ref().map(|v| v.to_mainline(pos_table)),
            metadata: term::record::FieldMetadata::from_ast(&field.metadata, pos_table)
                .with_field_name(name_innermost)
                .into(),
            pending_contracts: Vec::new(),
        };

        let mut it = field.path.iter();
        let fst = it.next().unwrap();

        let content = it.rev().fold(initial, |acc, path_elem| {
            // We first compute a position for the intermediate generated records (it's useful
            // in particular for the LSP). The position starts at the subpath corresponding to
            // the intermediate record and ends at the final value.
            let acc_pos = acc.value.as_ref().map(|value| value.pos(pos_table));

            let pos = acc_pos
                .map(|p| p.fuse(path_elem.pos()))
                .unwrap_or_else(|| path_elem.pos());

            match path_elem {
                FieldPathElem::Ident(id) => {
                    let mut fields = IndexMap::new();
                    fields.insert(*id, acc);
                    let pos_idx = pos_table.push(pos);
                    term::record::Field::from(
                        // See [^closurize-insertion]
                        closurize(NickelValue::record(
                            term::record::RecordData {
                                fields,
                                ..Default::default()
                            },
                            pos_idx,
                        )),
                    )
                }
                FieldPathElem::Expr(expr) => {
                    let pos = expr.pos;
                    let expr = NickelValue::from_ast(expr, pos_table);
                    let static_access = expr
                        .as_term()
                        .and_then(term::Term::try_str_chunk_as_static_str);

                    if let Some(static_access) = static_access {
                        let id = LocIdent::new_with_pos(static_access, pos);
                        let mut fields = IndexMap::new();
                        fields.insert(id, acc);

                        term::record::Field::from(
                            // See [^closurize-insertion]
                            closurize(NickelValue::record(
                                term::record::RecordData {
                                    fields,
                                    ..Default::default()
                                },
                                pos_table.push(pos),
                            )),
                        )
                    } else {
                        // The record we create isn't recursive, because it is only comprised of
                        // one dynamic field. It's just simpler to use the infrastructure of
                        // `RecRecord` to handle dynamic fields at evaluation time rather than
                        // right here
                        term::record::Field::from(NickelValue::term(
                            term::Term::rec_record(
                                term::record::RecordData::empty(),
                                Vec::new(),
                                vec![(expr, acc)],
                                None,
                                false,
                            ),
                            pos_table.push(pos),
                        ))
                    }
                }
            }
        });

        (fst.to_mainline(pos_table), content)
    }
}

impl<'ast> FromAst<record::FieldMetadata<'ast>> for term::record::FieldMetadata {
    fn from_ast(metadata: &record::FieldMetadata<'ast>, pos_table: &mut PosTable) -> Self {
        term::record::FieldMetadata {
            doc: metadata.doc.as_ref().map(|doc| Rc::from(*doc)),
            annotation: metadata.annotation.to_mainline(pos_table),
            opt: metadata.opt,
            not_exported: metadata.not_exported,
            priority: metadata.priority.clone(),
        }
    }
}

impl<'ast> FromAst<record::Include<'ast>> for term::record::Include {
    fn from_ast(incl: &record::Include<'ast>, pos_table: &mut PosTable) -> Self {
        term::record::Include {
            ident: incl.ident,
            metadata: incl.metadata.to_mainline(pos_table),
        }
    }
}

impl<'ast> FromAst<Type<'ast>> for mline_type::Type {
    fn from_ast(typ: &Type<'ast>, pos_table: &mut PosTable) -> Self {
        mline_type::Type {
            typ: typ.typ.to_mainline(pos_table),
            pos: typ.pos,
        }
    }
}

impl<'ast> FromAst<TypeUnr<'ast>> for MainlineTypeUnr {
    fn from_ast(typ: &TypeUnr<'ast>, pos_table: &mut PosTable) -> Self {
        typ.clone().map_state(
            |typ, pos_table| Box::new(typ.to_mainline(pos_table)),
            |rrows, pos_table| rrows.to_mainline(pos_table),
            |erows, pos_table| erows.to_mainline(pos_table),
            |ctr, pos_table| ctr.to_mainline(pos_table),
            pos_table,
        )
    }
}

impl<'ast> FromAst<RecordRows<'ast>> for mline_type::RecordRows {
    fn from_ast(rrows: &RecordRows<'ast>, pos_table: &mut PosTable) -> Self {
        mline_type::RecordRows(rrows.0.to_mainline(pos_table))
    }
}

impl<'ast> FromAst<EnumRows<'ast>> for mline_type::EnumRows {
    fn from_ast(erows: &EnumRows<'ast>, pos_table: &mut PosTable) -> Self {
        mline_type::EnumRows(erows.0.to_mainline(pos_table))
    }
}

impl<'ast> FromAst<EnumRowsUnr<'ast>> for MainlineEnumRowsUnr {
    fn from_ast(erows: &EnumRowsUnr<'ast>, pos_table: &mut PosTable) -> Self {
        erows.clone().map_state(
            |typ, pos_table| Box::new(typ.to_mainline(pos_table)),
            |erows, pos_table| Box::new(erows.to_mainline(pos_table)),
            pos_table,
        )
    }
}

impl<'ast> FromAst<EnumRow<'ast>> for mline_type::EnumRow {
    fn from_ast(erow: &EnumRow<'ast>, pos_table: &mut PosTable) -> Self {
        mline_type::EnumRow(EnumRowF {
            id: erow.id,
            typ: erow
                .typ
                .as_ref()
                .map(|ty| Box::new((*ty).to_mainline(pos_table))),
        })
    }
}

impl<'ast> FromAst<RecordRowsUnr<'ast>> for MainlineRecordRowsUnr {
    fn from_ast(rrows: &RecordRowsUnr<'ast>, pos_table: &mut PosTable) -> Self {
        rrows.clone().map_state(
            |typ, pos_table| Box::new(typ.to_mainline(pos_table)),
            |rrows, pos_table| Box::new(rrows.to_mainline(pos_table)),
            pos_table,
        )
    }
}

impl<'ast> FromAst<RecordRow<'ast>> for mline_type::RecordRow {
    fn from_ast(rrow: &RecordRow<'ast>, pos_table: &mut PosTable) -> Self {
        mline_type::RecordRow(RecordRowF {
            id: rrow.id,
            typ: Box::new(rrow.typ.to_mainline(pos_table)),
        })
    }
}

impl<'ast> FromAst<Type<'ast>> for term::LabeledType {
    fn from_ast(typ: &Type<'ast>, pos_table: &mut PosTable) -> Self {
        let typ: mline_type::Type = typ.to_mainline(pos_table);
        //TODO:remove
        if typ.pos.into_opt().is_none() {
            panic!("Expected a position to be set for the type {typ:?}");
        }
        // We expect the new AST node to always have a position set. In fact we should
        // probably switch to `RawSpan` instead of `TermPos` everywhere; but let's do that
        // later
        let span = typ.pos.unwrap();

        term::LabeledType {
            typ: typ.clone(),
            label: label::Label {
                typ: std::rc::Rc::new(typ),
                span: Some(span),
                ..Default::default()
            },
        }
    }
}

/// One data type representing all possible primops from the mainline AST, whether unary, binary or
/// multi-ary.
enum TermPrimOp {
    Unary(term::UnaryOp),
    Binary(term::BinaryOp),
    NAry(term::NAryOp),
}

impl FromAst<PrimOp> for TermPrimOp {
    fn from_ast(op: &PrimOp, _pos_table: &mut PosTable) -> Self {
        match op {
            PrimOp::Typeof => TermPrimOp::Unary(term::UnaryOp::Typeof),
            PrimOp::Cast => TermPrimOp::Unary(term::UnaryOp::Cast),
            PrimOp::BoolAnd => TermPrimOp::Unary(term::UnaryOp::BoolAnd),
            PrimOp::BoolOr => TermPrimOp::Unary(term::UnaryOp::BoolOr),
            PrimOp::BoolNot => TermPrimOp::Unary(term::UnaryOp::BoolNot),
            PrimOp::Blame => TermPrimOp::Unary(term::UnaryOp::Blame),
            PrimOp::EnumEmbed(loc_ident) => TermPrimOp::Unary(term::UnaryOp::EnumEmbed(*loc_ident)),
            PrimOp::RecordStatAccess(loc_ident) => {
                TermPrimOp::Unary(term::UnaryOp::RecordAccess(*loc_ident))
            }
            PrimOp::ArrayMap => TermPrimOp::Unary(term::UnaryOp::ArrayMap),
            PrimOp::RecordMap => TermPrimOp::Unary(term::UnaryOp::RecordMap),
            PrimOp::LabelFlipPol => TermPrimOp::Unary(term::UnaryOp::LabelFlipPol),
            PrimOp::LabelPol => TermPrimOp::Unary(term::UnaryOp::LabelPol),
            PrimOp::LabelGoDom => TermPrimOp::Unary(term::UnaryOp::LabelGoDom),
            PrimOp::LabelGoCodom => TermPrimOp::Unary(term::UnaryOp::LabelGoCodom),
            PrimOp::LabelGoArray => TermPrimOp::Unary(term::UnaryOp::LabelGoArray),
            PrimOp::LabelGoDict => TermPrimOp::Unary(term::UnaryOp::LabelGoDict),
            PrimOp::Seq => TermPrimOp::Unary(term::UnaryOp::Seq),
            PrimOp::DeepSeq => TermPrimOp::Unary(term::UnaryOp::DeepSeq),
            PrimOp::ArrayLength => TermPrimOp::Unary(term::UnaryOp::ArrayLength),
            PrimOp::ArrayGen => TermPrimOp::Unary(term::UnaryOp::ArrayGen),
            PrimOp::RecordFields(record_op_kind) => {
                TermPrimOp::Unary(term::UnaryOp::RecordFields(*record_op_kind))
            }
            PrimOp::RecordValues => TermPrimOp::Unary(term::UnaryOp::RecordValues),
            PrimOp::StringTrim => TermPrimOp::Unary(term::UnaryOp::StringTrim),
            PrimOp::StringChars => TermPrimOp::Unary(term::UnaryOp::StringChars),
            PrimOp::StringUppercase => TermPrimOp::Unary(term::UnaryOp::StringUppercase),
            PrimOp::StringLowercase => TermPrimOp::Unary(term::UnaryOp::StringLowercase),
            PrimOp::StringLength => TermPrimOp::Unary(term::UnaryOp::StringLength),
            PrimOp::ToString => TermPrimOp::Unary(term::UnaryOp::ToString),
            PrimOp::NumberFromString => TermPrimOp::Unary(term::UnaryOp::NumberFromString),
            PrimOp::EnumFromString => TermPrimOp::Unary(term::UnaryOp::EnumFromString),
            PrimOp::StringIsMatch => TermPrimOp::Unary(term::UnaryOp::StringIsMatch),
            PrimOp::StringFind => TermPrimOp::Unary(term::UnaryOp::StringFind),
            PrimOp::StringFindAll => TermPrimOp::Unary(term::UnaryOp::StringFindAll),
            PrimOp::Force {
                ignore_not_exported,
            } => TermPrimOp::Unary(term::UnaryOp::Force {
                ignore_not_exported: *ignore_not_exported,
            }),
            PrimOp::RecordEmptyWithTail => TermPrimOp::Unary(term::UnaryOp::RecordEmptyWithTail),
            PrimOp::RecordFreeze => TermPrimOp::Unary(term::UnaryOp::RecordFreeze),
            PrimOp::Trace => TermPrimOp::Unary(term::UnaryOp::Trace),
            PrimOp::LabelPushDiag => TermPrimOp::Unary(term::UnaryOp::LabelPushDiag),
            PrimOp::EnumGetArg => TermPrimOp::Unary(term::UnaryOp::EnumGetArg),
            PrimOp::EnumMakeVariant => TermPrimOp::Unary(term::UnaryOp::EnumMakeVariant),
            PrimOp::EnumIsVariant => TermPrimOp::Unary(term::UnaryOp::EnumIsVariant),
            PrimOp::EnumGetTag => TermPrimOp::Unary(term::UnaryOp::EnumGetTag),
            PrimOp::ContractCustom => TermPrimOp::Unary(term::UnaryOp::ContractCustom),
            PrimOp::NumberArcCos => TermPrimOp::Unary(term::UnaryOp::NumberArcCos),
            PrimOp::NumberArcSin => TermPrimOp::Unary(term::UnaryOp::NumberArcSin),
            PrimOp::NumberArcTan => TermPrimOp::Unary(term::UnaryOp::NumberArcTan),
            PrimOp::NumberCos => TermPrimOp::Unary(term::UnaryOp::NumberCos),
            PrimOp::NumberSin => TermPrimOp::Unary(term::UnaryOp::NumberSin),
            PrimOp::NumberTan => TermPrimOp::Unary(term::UnaryOp::NumberTan),
            #[cfg(feature = "nix-experimental")]
            PrimOp::EvalNix => TermPrimOp::Unary(term::UnaryOp::EvalNix),

            // Binary operations
            PrimOp::Plus => TermPrimOp::Binary(term::BinaryOp::Plus),
            PrimOp::Sub => TermPrimOp::Binary(term::BinaryOp::Sub),
            PrimOp::Mult => TermPrimOp::Binary(term::BinaryOp::Mult),
            PrimOp::Div => TermPrimOp::Binary(term::BinaryOp::Div),
            PrimOp::Modulo => TermPrimOp::Binary(term::BinaryOp::Modulo),
            PrimOp::NumberArcTan2 => TermPrimOp::Binary(term::BinaryOp::NumberArcTan2),
            PrimOp::NumberLog => TermPrimOp::Binary(term::BinaryOp::NumberLog),
            PrimOp::Pow => TermPrimOp::Binary(term::BinaryOp::Pow),
            PrimOp::StringConcat => TermPrimOp::Binary(term::BinaryOp::StringConcat),
            PrimOp::Eq => TermPrimOp::Binary(term::BinaryOp::Eq),
            PrimOp::LessThan => TermPrimOp::Binary(term::BinaryOp::LessThan),
            PrimOp::LessOrEq => TermPrimOp::Binary(term::BinaryOp::LessOrEq),
            PrimOp::GreaterThan => TermPrimOp::Binary(term::BinaryOp::GreaterThan),
            PrimOp::GreaterOrEq => TermPrimOp::Binary(term::BinaryOp::GreaterOrEq),
            PrimOp::ContractApply => TermPrimOp::Binary(term::BinaryOp::ContractApply),
            PrimOp::ContractCheck => TermPrimOp::Binary(term::BinaryOp::ContractCheck),
            PrimOp::LabelWithErrorData => TermPrimOp::Binary(term::BinaryOp::LabelWithErrorData),
            PrimOp::LabelGoField => TermPrimOp::Binary(term::BinaryOp::LabelGoField),
            PrimOp::RecordInsert(record_op_kind) => {
                TermPrimOp::Binary(term::BinaryOp::RecordInsert {
                    metadata: Default::default(),
                    pending_contracts: Vec::new(),
                    ext_kind: term::RecordExtKind::WithValue,
                    op_kind: *record_op_kind,
                })
            }
            PrimOp::RecordRemove(record_op_kind) => {
                TermPrimOp::Binary(term::BinaryOp::RecordRemove(*record_op_kind))
            }
            PrimOp::RecordGet => TermPrimOp::Binary(term::BinaryOp::RecordGet),
            PrimOp::RecordHasField(record_op_kind) => {
                TermPrimOp::Binary(term::BinaryOp::RecordHasField(*record_op_kind))
            }
            PrimOp::RecordFieldIsDefined(record_op_kind) => {
                TermPrimOp::Binary(term::BinaryOp::RecordFieldIsDefined(*record_op_kind))
            }
            PrimOp::RecordSplitPair => TermPrimOp::Binary(term::BinaryOp::RecordSplitPair),
            PrimOp::RecordDisjointMerge => TermPrimOp::Binary(term::BinaryOp::RecordDisjointMerge),
            PrimOp::ArrayConcat => TermPrimOp::Binary(term::BinaryOp::ArrayConcat),
            PrimOp::ArrayAt => TermPrimOp::Binary(term::BinaryOp::ArrayAt),
            PrimOp::Merge(merge_kind) => {
                // [^merge-label-span] The mainline AST requires a `MergeLabel` object, itself
                // demanding a `RawSpan` that we can't provide here - it's stored higher up in the
                // AST, at the `PrimOpApp` node. We generate a dummy span and rely on the caller
                // (in practice `FromAst<Node<'_>>`) to post-process a merge primop application,
                // setting the span of the dummy merge label correctly.
                let dummy_label: label::MergeLabel = label::Label::dummy().into();

                TermPrimOp::Binary(term::BinaryOp::Merge(label::MergeLabel {
                    kind: *merge_kind,
                    ..dummy_label
                }))
            }
            PrimOp::Hash => TermPrimOp::Binary(term::BinaryOp::Hash),
            PrimOp::Serialize => TermPrimOp::Binary(term::BinaryOp::Serialize),
            PrimOp::Deserialize => TermPrimOp::Binary(term::BinaryOp::Deserialize),
            PrimOp::StringSplit => TermPrimOp::Binary(term::BinaryOp::StringSplit),
            PrimOp::StringContains => TermPrimOp::Binary(term::BinaryOp::StringContains),
            PrimOp::StringCompare => TermPrimOp::Binary(term::BinaryOp::StringCompare),
            PrimOp::Seal => TermPrimOp::Binary(term::BinaryOp::Seal),
            PrimOp::Unseal => TermPrimOp::Binary(term::BinaryOp::Unseal),
            PrimOp::ContractArrayLazyApp => {
                TermPrimOp::Binary(term::BinaryOp::ContractArrayLazyApp)
            }
            PrimOp::ContractRecordLazyApp => {
                TermPrimOp::Binary(term::BinaryOp::ContractRecordLazyApp)
            }
            PrimOp::LabelWithMessage => TermPrimOp::Binary(term::BinaryOp::LabelWithMessage),
            PrimOp::LabelWithNotes => TermPrimOp::Binary(term::BinaryOp::LabelWithNotes),
            PrimOp::LabelAppendNote => TermPrimOp::Binary(term::BinaryOp::LabelAppendNote),
            PrimOp::LabelLookupTypeVar => TermPrimOp::Binary(term::BinaryOp::LabelLookupTypeVar),

            // N-ary operations
            PrimOp::StringReplace => TermPrimOp::NAry(term::NAryOp::StringReplace),
            PrimOp::StringReplaceRegex => TermPrimOp::NAry(term::NAryOp::StringReplaceRegex),
            PrimOp::StringSubstr => TermPrimOp::NAry(term::NAryOp::StringSubstr),
            PrimOp::StringBase64Encode => TermPrimOp::Binary(term::BinaryOp::StringBase64Encode),
            PrimOp::StringBase64Decode => TermPrimOp::Binary(term::BinaryOp::StringBase64Decode),
            PrimOp::MergeContract => TermPrimOp::NAry(term::NAryOp::MergeContract),
            PrimOp::RecordSealTail => TermPrimOp::NAry(term::NAryOp::RecordSealTail),
            PrimOp::RecordUnsealTail => TermPrimOp::NAry(term::NAryOp::RecordUnsealTail),
            PrimOp::LabelInsertTypeVar => TermPrimOp::NAry(term::NAryOp::LabelInsertTypeVar),
            PrimOp::ArraySlice => TermPrimOp::NAry(term::NAryOp::ArraySlice),
        }
    }
}

impl<'ast> FromAst<Ast<'ast>> for NickelValue {
    fn from_ast(ast: &Ast<'ast>, pos_table: &mut PosTable) -> Self {
        use term::Term;

        let mut result = match &ast.node {
            Node::Null => NickelValue::null().with_pos_idx(pos_table.push(ast.pos)),
            Node::Bool(b) => NickelValue::bool_value(*b, pos_table.push(ast.pos)),
            Node::Number(n) => NickelValue::number((**n).clone(), pos_table.push(ast.pos)),
            Node::String(s) => NickelValue::string(*s, pos_table.push(ast.pos)),
            Node::StringChunks(chunks) => {
                let chunks = chunks
                    .iter()
                    .rev()
                    .map(|chunk| match chunk {
                        StringChunk::Literal(s) => term::StrChunk::Literal(s.clone()),
                        StringChunk::Expr(expr, indent) => {
                            term::StrChunk::Expr(expr.to_mainline(pos_table), *indent)
                        }
                    })
                    .collect();

                NickelValue::term(Term::StrChunks(chunks), pos_table.push(ast.pos))
            }
            Node::Fun { args, body } => {
                let body_pos = body.pos;

                // We transform a n-ary function representation to a chain of nested unary
                // functions, the latter being the representation used in the mainline AST.
                args.iter()
                    .rev()
                    .fold(NickelValue::from_ast(body, pos_table), |acc, arg| {
                        let term = match arg.data {
                            PatternData::Any(id) => Term::fun(id, acc),
                            _ => term::pattern::compile::compile_fun_pattern(pos_table, arg, acc),
                        };

                        // [^nary-constructors-unrolling]: this case is a bit annoying: we need to
                        // extract the position of the intermediate created functions to satisfy
                        // the old AST structure, but this information isn't available directly.
                        //
                        // What we do here is to fuse the span of the term being built and the one
                        // of the current argument, which should be a reasonable approximation (if
                        // not exactly the same thing).
                        NickelValue::term(term, pos_table.push(arg.pos.fuse(body_pos)))
                    })
                    .with_pos_idx(pos_table.push(ast.pos))
            }
            Node::Let {
                bindings,
                body,
                rec,
            } => {
                // Mainline term bindings can't have any metadata associated with them. We need to
                // rewrite let metadata to be free-standing type and contract annotations instead,
                // which is achieved by this helper.
                fn with_metadata(
                    pos_table: &mut PosTable,
                    metadata: &LetMetadata<'_>,
                    value: &Ast<'_>,
                ) -> NickelValue {
                    let value: NickelValue = value.to_mainline(pos_table);
                    let pos = value.pos(pos_table);

                    if metadata.annotation.is_empty() {
                        return value;
                    }

                    NickelValue::term(
                        term::Term::annotated(metadata.annotation.to_mainline(pos_table), value),
                        pos_table.push(pos),
                    )
                }

                // We try to collect all patterns as single identifiers. If this works, we can emit
                // a simpler / more compact `Let`.
                let try_bindings = bindings
                    .iter()
                    .map(
                        |LetBinding {
                             pattern,
                             metadata,
                             value,
                         }| match pattern.data {
                            PatternData::Any(id) => {
                                Some((id, with_metadata(pos_table, metadata, value)))
                            }
                            _ => None,
                        },
                    )
                    .collect::<Option<SmallVec<_>>>();

                let body = body.to_mainline(pos_table);
                let attrs = term::LetAttrs {
                    rec: *rec,
                    ..Default::default()
                };

                let term = if let Some(bindings) = try_bindings {
                    Term::let_in(bindings, body, attrs)
                } else {
                    let bindings = bindings
                        .iter()
                        .map(
                            |LetBinding {
                                 pattern,
                                 value,
                                 metadata,
                             }| {
                                (pattern, with_metadata(pos_table, metadata, value))
                            },
                        )
                        .collect::<Vec<_>>();

                    term::pattern::compile::compile_let_pattern(pos_table, &bindings, body, attrs)
                };

                NickelValue::term(term, pos_table.push(ast.pos))
            }
            Node::App { head, args } => {
                let head_pos = head.pos;

                // We transform a n-ary application representation to a chain of nested unary
                // applications, the latter being the representation used in the mainline AST.
                args.iter()
                    .fold(head.to_mainline(pos_table), |result, arg| {
                        // see [^nary-constructors-unrolling]
                        let arg_pos = arg.pos;
                        NickelValue::term(
                            Term::app(result, arg.to_mainline(pos_table)),
                            pos_table.push(head_pos.fuse(arg_pos)),
                        )
                    })
            }
            Node::Var(loc_ident) => {
                NickelValue::term(Term::Var(*loc_ident), pos_table.push(ast.pos))
            }
            Node::EnumVariant { tag, arg } => NickelValue::enum_variant(
                *tag,
                arg.map(|arg| arg.to_mainline(pos_table)),
                pos_table.push(ast.pos),
            ),
            Node::Record(record) => {
                let (data, dyn_fields) = (*record).to_mainline(pos_table);
                NickelValue::term(
                    Term::rec_record(
                        data,
                        record
                            .includes
                            .iter()
                            .map(|incl| incl.to_mainline(pos_table))
                            .collect(),
                        dyn_fields,
                        None,
                        false,
                    ),
                    pos_table.push(ast.pos),
                )
            }
            Node::IfThenElse {
                cond,
                then_branch,
                else_branch,
            } => term::make::if_then_else(
                NickelValue::from_ast(cond, pos_table),
                NickelValue::from_ast(then_branch, pos_table),
                NickelValue::from_ast(else_branch, pos_table),
            ),
            Node::Match(data) => {
                let branches = data
                    .branches
                    .iter()
                    .map(|branch| term::pattern::compile::MatchBranch {
                        pattern: branch.pattern.clone(),
                        guard: branch.guard.clone().map(|g| g.to_mainline(pos_table)),
                        body: branch.body.to_mainline(pos_table),
                    })
                    .collect();
                let match_data = term::pattern::compile::MatchData { branches };
                let arg = LocIdent::fresh();
                let pos_idx = pos_table.push(ast.pos);
                NickelValue::term(
                    Term::Fun(term::FunData {
                        arg,
                        body: match_data.compile(pos_table, Term::Var(arg).into(), pos_idx),
                    }),
                    pos_idx,
                )
            }
            Node::Array(array) => {
                let pos_idx = pos_table.push(ast.pos);
                let array = array.iter().map(|elt| elt.to_mainline(pos_table)).collect();

                // [^closurize-insertion]: When converting from the AST, we need to generate
                // well-formed values. In particular, containers (arrays and non-recursive records)
                // must be closurized the first time they are evaluated. So instead of being
                // translated as arrays directly, we wrap them using the `Closurize` operation.
                closurize(NickelValue::array(array, Vec::new(), pos_idx))
            }
            Node::PrimOpApp { op, args } => {
                let term = match (*op).to_mainline(pos_table) {
                    TermPrimOp::Unary(op) => Term::op1(op, args[0].to_mainline(pos_table)),
                    // If `op` is `Merge`, we need to patch the span of the merge label with the
                    // correct value. Unfortunately, we still don't have access to the right span,
                    // which is the position of this whole node. We delegate this to the caller, that
                    // is `from_ast::<Ast<'ast>>`. See [^merge-label-span].
                    TermPrimOp::Binary(op) => Term::op2(
                        op,
                        args[0].to_mainline(pos_table),
                        args[1].to_mainline(pos_table),
                    ),
                    TermPrimOp::NAry(op) => Term::opn(
                        op,
                        args.iter()
                            .map(|arg| (*arg).to_mainline(pos_table))
                            .collect(),
                    ),
                };

                NickelValue::term(term, pos_table.push(ast.pos))
            }
            Node::Annotated { annot, inner } => NickelValue::term(
                Term::annotated(
                    (*annot).to_mainline(pos_table),
                    inner.to_mainline(pos_table),
                ),
                pos_table.push(ast.pos),
            ),
            Node::Import(Import::Path { path, format }) => NickelValue::term(
                Term::Import(term::Import::Path {
                    path: (*path).to_owned(),
                    format: *format,
                }),
                pos_table.push(ast.pos),
            ),
            Node::Import(Import::Package { id }) => NickelValue::term(
                Term::Import(term::Import::Package { id: *id }),
                pos_table.push(ast.pos),
            ),
            Node::Type(typ) => {
                let typ: mline_type::Type = (*typ).to_mainline(pos_table);

                let contract = typ
                    .contract(pos_table)
                    // It would be painful to change the interface of `ToMainline` and make it
                    // fallible just for this one special case. Instead, if the contract conversion
                    // causes an unbound variable error (which it shouldn't anyway if the term has
                    // been correctly typechecked), we pack this error as a parse error in the AST.
                    .unwrap_or_else(|err| {
                        Term::parse_error(ParseError::UnboundTypeVariables(vec![err.0])).into()
                    });

                NickelValue::typ(typ, contract, pos_table.push(ast.pos))
            }
            Node::ParseError(error) => {
                NickelValue::term(Term::parse_error((*error).clone()), pos_table.push(ast.pos))
            }
        };

        // See [^merge-label-span]
        if let ValueContentRefMut::Term(term::Term::Op2(data)) = result.content_make_mut()
            && let term::BinaryOp::Merge(label) = &mut data.op
        {
            label.span = ast.pos.into_opt();
        }

        result
    }
}

impl<'ast> FromAst<&'ast Ast<'ast>> for NickelValue {
    fn from_ast(ast: &&'ast Ast<'ast>, pos_table: &mut PosTable) -> Self {
        FromAst::from_ast(*ast, pos_table)
    }
}

/// Convert a record definition to a mainline recursive record definition (with static and dynamic
/// fields). If a field is defined several times, the different definitions are merged, statically
/// if possible.
impl<'ast> FromAst<Record<'ast>>
    for (
        term::record::RecordData,
        Vec<(NickelValue, term::record::Field)>,
    )
{
    fn from_ast(record: &Record<'ast>, pos_table: &mut PosTable) -> Self {
        use indexmap::map::Entry;

        fn insert_static_field(
            static_fields: &mut IndexMap<LocIdent, term::record::Field>,
            id: LocIdent,
            field: term::record::Field,
        ) {
            match static_fields.entry(id) {
                Entry::Occupied(mut occpd) => {
                    // temporarily putting an empty field in the entry to take the previous value.
                    let prev = occpd.insert(term::record::Field::default());

                    // unwrap(): the field's identifier must have a position during parsing.
                    occpd.insert(merge_fields(id.pos.unwrap(), prev, field));
                }
                Entry::Vacant(vac) => {
                    vac.insert(field);
                }
            }
        }

        let mut static_fields = IndexMap::new();
        let mut dynamic_fields = Vec::new();

        for def in record.field_defs {
            match def.to_mainline(pos_table) {
                (FieldName::Ident(id), field) => insert_static_field(&mut static_fields, id, field),
                (FieldName::Expr(expr), field) => {
                    let pos = expr.pos(pos_table);
                    // Dynamic fields (whose name is defined by an interpolated string) have a different
                    // semantics than fields whose name can be determined statically. However, static
                    // fields with special characters are also parsed as string chunks:
                    //
                    // ```
                    // let x = "dynamic" in {"I%am.static" = false, "%{x}" = true}
                    // ```
                    //
                    // Here, both fields are parsed as `StrChunks`, but the first field is actually a
                    // static one, just with special characters. The following code determines which fields
                    // are actually static or not, and inserts them in the right location.
                    let static_access = expr
                        .as_term()
                        .and_then(term::Term::try_str_chunk_as_static_str);

                    if let Some(static_access) = static_access {
                        insert_static_field(
                            &mut static_fields,
                            LocIdent::new_with_pos(static_access, pos),
                            field,
                        )
                    } else {
                        dynamic_fields.push((expr, field));
                    }
                }
            }
        }

        (
            term::record::RecordData::new(
                static_fields,
                term::record::RecordAttrs {
                    open: record.open,
                    ..Default::default()
                },
                None,
            ),
            dynamic_fields,
        )
    }
}

/// Merge two fields by performing the merge of both their value (statically whenever possible, but
/// dynamically if necessary, by introducing a merge operator) and their metadata (statically).
///
/// If the values of both fields are static records ([`term::Term::Record`]s), their merge is
/// computed statically. This prevents building terms whose depth is linear in the number of fields
/// if partial definitions are involved. This manifested in
/// https://github.com/tweag/nickel/issues/1427.
///
/// This is a helper for the conversion of a record definition to mainline.
fn merge_fields(
    id_span: RawSpan,
    field1: term::record::Field,
    field2: term::record::Field,
) -> term::record::Field {
    use crate::eval::{
        merge::{merge_doc, split},
        value::ValueContent,
    };
    use term::{BinaryOp, make as mk_term, record::RecordData};

    // FIXME: We're duplicating a lot of the logic in
    // [`eval::merge::merge_fields`] but not quite enough to actually factor
    // it out
    fn merge_values(id_span: RawSpan, v1: NickelValue, v2: NickelValue) -> NickelValue {
        match (v1.content(), v2.content()) {
            (ValueContent::Record(lens1), ValueContent::Record(lens2)) => {
                let rd1 = lens1.take().unwrap_or_alloc();
                let rd2 = lens2.take().unwrap_or_alloc();

                let split::SplitResult {
                    left,
                    center,
                    right,
                } = split::split(rd1.fields, rd2.fields);
                let mut fields = IndexMap::with_capacity(left.len() + center.len() + right.len());
                fields.extend(left);
                fields.extend(right);
                for (id, (field1, field2)) in center.into_iter() {
                    fields.insert(id, merge_fields(id_span, field1, field2));
                }

                // See [^closurize-insertion]
                closurize(NickelValue::record_posless(RecordData::new(
                    fields,
                    Combine::combine(rd1.attrs, rd2.attrs),
                    None,
                )))
            }
            (lens1, lens2) => mk_term::op2(
                BinaryOp::Merge(label::MergeLabel {
                    span: Some(id_span),
                    kind: ast::MergeKind::PiecewiseDef,
                }),
                lens1.restore(),
                lens2.restore(),
            ),
        }
    }

    let metadata1 = field1.metadata.into_inner();
    let metadata2 = field2.metadata.into_inner();

    let (value, priority) = match (field1.value, field2.value) {
        (Some(t1), Some(t2)) if metadata1.priority == metadata2.priority => {
            (Some(merge_values(id_span, t1, t2)), metadata1.priority)
        }
        (Some(t), _) if metadata1.priority > metadata2.priority => (Some(t), metadata1.priority),
        (_, Some(t)) if metadata1.priority < metadata2.priority => (Some(t), metadata2.priority),
        (Some(t), None) => (Some(t), metadata1.priority),
        (None, Some(t)) => (Some(t), metadata2.priority),
        (None, None) => (None, Default::default()),
        _ => unreachable!(),
    };

    // At this stage, pending contracts aren't filled nor meaningful, and should all be empty.
    debug_assert!(field1.pending_contracts.is_empty() && field2.pending_contracts.is_empty());

    term::record::Field {
        value,
        // [`FieldMetadata::combine`] produces subtly different behaviour from the runtime merging
        // code: we don't use [`Combine::combine`] here and replicate the merging logic instead.
        metadata: term::record::FieldMetadata {
            doc: merge_doc(metadata1.doc, metadata2.doc),
            annotation: Combine::combine(metadata1.annotation, metadata2.annotation),
            opt: metadata1.opt && metadata2.opt,
            not_exported: metadata1.not_exported || metadata2.not_exported,
            priority,
        }
        .into(),
        pending_contracts: Vec::new(),
    }
}

/// Wrap a value in a [crate::term::Term::Closurize] operator with the same position index.
fn closurize(value: NickelValue) -> NickelValue {
    let pos_idx = value.pos_idx();

    NickelValue::term(term::Term::Closurize(value), pos_idx)
}
